<?php
/**
 * data adaptor for PDO
 * @author bbqq <toughgamer@gmail.com>
 * @package com.blib.data
 * @copyright bbqq qstufie.com
 * @implements <Blib_Interface_Data_Adaptor>
 */
abstract class Blib_Data_Adaptor_Pdo extends Blib_Class_Atomic implements Blib_Interface_Data_Adaptor
{
    /**
     * pdo connection object
     * @var PDO
     */
    protected $_pdo = null;


    /**
     * pdo statement object
     * @var PDOStatement
     */
    protected $_stmt = null;


    /**
     * construct the adaptor
     *
     * @param array $options
     *
     * @return void
     */
    abstract public function __construct($options = array());// end __construct


    // get the last insert id...
    public function getLastInsertId()
    {
        return (int) $this->_pdo->lastInsertId();
        // support: mysql, sqlite, pgsql

    }// end getLastInsertId;


    // quote names... such as `` or ""
    abstract public function nameQuote($name);


    /**
     * (non-PHPdoc)
     * @see lib/dao/adaptor/Blib_Interface_Data_Adaptor#query($sql, $bind)
     */
    public function query($sql, $bind = array())
    {
        // why passthru? so i can do bench marking!
        // check if bench allowed, if not, passthru directly...
        if (!Blib::isDebugAllowed()) {
            return $this->_query($sql, $bind);
        }

        // otherwise, do the bench...
        // start bench here...
        $mtime = Blib_Utility_Date::getMicroTime('', true);
        // do the job
        $return = $this->_query($sql, $bind);
        $intval = number_format(Blib_Utility_Date::getMicroTime('', true) - $mtime, 4);
        $msg    = 'Query: <pre>' . $sql . '</pre><br />' .
                  'Bind: &nbsp;<pre>' . var_export($bind, 1) . '</pre>';
        Blib::_bset($msg, $intval);
        return $return;

    }// end query


    /**
     * (non-PHPdoc)
     * @see lib/dao/adaptor/Blib_Interface_Data_Adaptor#query($sql, $bind)
     */
    public function _query($sql, $bind = array())
    {
        // test
        // AppCore::dump($sql, $bind);
        // prepare...
        try {
            $this->_stmt = $this->_pdo->prepare($sql);
            if (is_object($this->_stmt) && $this->_stmt->execute($bind)) {
                // now, do i need to clear buffer?
                if (strpos(strtolower($sql), 'select') === false) {
                    // should clear it!
                    $this->_stmt->fetch();
                }

                return true;
            }

            // otherwise, check error
            $err = 'unknown PDO error';
            if (is_object($this->_pdo)) {
                $err  = (array) $this->_pdo->errorInfo();
                $code = !empty($err[1]) ? (int) $err[1] : null;
                $err  = !empty($err[2]) ? $err[2] : null;
            }

            $code = 100;
            if (is_object($this->_stmt)) {
                $err  = $this->_stmt->errorInfo();
                $err  = !empty($err[2]) ? $err[2] : null;
                $code = $this->_stmt->errorCode();
            }

            throw new Blib_Exception_Dao($sql, $bind, new Exception('SQL ERROR: ' . $err, (int) $code));

        } catch (Exception $e) {
            throw new Blib_Exception_Dao($sql, $bind, $e);
        }

    }// end _query


    // quote some string
    public function quote($str)
    {
        return $this->_pdo->quote($str);

    }// end quote


    /**
     * (non-PHPdoc)
     * @see lib/dao/adaptor/Blib_Interface_Data_Adaptor#insert($data, $tableName)
     */
    public function insert($data, $tableName)
    {
        // break down...
        $breakDown = $this->_breakDownData($data, $tableName, 'insert');

        // run
        $result = $this->query($breakDown['sql'], $breakDown['bind']);

        // insert id...
        $id = (int) $this->getLastInsertId();
        if ($id > 0) {
            return $id;
        }

        return $result;

    }// end insert


    /**
     * replace into the db...
     * @param array $data the source data
     * @param str $tableName the actual table name
     * @return bool
     */
    public function replaceInto($data, $tableName)
    {
        // break down...
        $breakDown = $this->_breakDownData($data, $tableName, 'replace_into');

        // run
        return $this->query($breakDown['sql'], $breakDown['bind']);

    }// end replaceInto


    /**
     * (non-PHPdoc)
     * @see lib/dao/adaptor/Blib_Interface_Data_Adaptor#update($data, $tableName)
     */
    public function update($data, $tableName, $condition)
    {
        // condition must be there!!!
        if (empty($condition)) {
            return $this->_error('UPDATE ' . $tableName, $data, 'Condition is empty');
        }

        // break down...
        $breakDown = $this->_breakDownData($data, $tableName, 'update');

        // run
        return $this->query($breakDown['sql'] . " WHERE {$condition}", $breakDown['bind']);

    }// end update


    /**
     * (non-PHPdoc)
     * @see lib/dao/adaptor/Blib_Interface_Data_Adaptor#delete($data, $tableName)
     */
    public function delete($tableName, $condition)
    {
        // condition must be there!!!
        if (empty($condition)) {
            return $this->_error('DELETE FROM ' . $tableName, null, 'Condition is empty');
        }

        // otherwise, delete!
        return $this->query('DELETE FROM ' . $tableName . ' WHERE ' . $condition . ';');

    }// end delete


    /**
     * (non-PHPdoc)
     * @see lib/dao/adaptor/Blib_Interface_Data_Adaptor#fetchOne($sql, $bind)
     */
    public function fetchOne($sql, $bind = array(), $cache = false)
    {
        // cached?
        if ($cache) {
            // try fetching it...
            $data = $this->_loadCache($sql, $bind);
            if ($data !== false) {
                return $data;
            }
        }

        // prepare statement
        try {
            if ($this->query($sql, $bind)) {
                // start fetching
                $result = $this->_stmt->fetch();
                // if cache...
                if ($cache) {
                    $this->_cache($sql, $bind, $result);
                }

                return $result;
            }
            // otherwise, check error
            $this->_error($sql, $bind);

        } catch (Exception $e) {
            throw new Blib_Exception_Dao($sql, $bind, $e);
        }

    }// end fetchOne


    /**
     * (non-PHPdoc)
     * @see lib/dao/adaptor/Blib_Interface_Data_Adaptor#fetchAll($sql, $bind)
     */
    public function fetchAll($sql, $bind = array(), $cache = false)
    {
        // cached?
        if ($cache) {
            // try fetching it...
            $data = $this->_loadCache($sql, $bind);
            if ($data !== false) {
                return $data;
            }
        }

        // prepare statement
        try {
            if ($this->query($sql, $bind)) {
                // start fetching
                $result = $this->_stmt->fetchAll();
                // if cache...
                if ($cache) {
                    $this->_cache($sql, $bind, $result);
                }

                return $result;
            }
            // otherwise, check error
            $this->_error($sql, $bind);

        } catch (Exception $e) {
            throw new Blib_Exception_Dao($sql, $bind, $e);
        }

    }// end fetchAll


    /**
     * (non-PHPdoc)
     * @see lib/dao/adaptor/Blib_Interface_Data_Adaptor#getRowCount()
     */
    public function getRowCount()
    {
        if ($this->_stmt instanceof PDOStatement) {
            return $this->_stmt->columnCount();
        }

    }// end getRowCount


    /**
     * throw Exception
     * @return Blib_Exception_Dao
     */
    public function _throw(Exception $e, $sql = null, $bind = array())
    {
        throw new Blib_Exception_Dao($sql, $bind, $e);

    }// end _throw


    /**
     * get error!
     * @param unknown_type $sql
     * @param unknown_type $bind
     * @return unknown_type
     */
    protected function _error($sql = null, $bind = array(), $msg = null)
    {
        if ($this->_stmt instanceof PDOStatement) {
            if (empty($msg)) {
                $msg  = $this->_stmt->errorInfo();
                $msg  = $msg[2];
            }
            $code = (int) $this->_stmt->errorCode();
            // now clean up the stmt
            $this->_stmt = null;
            $e    = new Exception($msg, $code);
            throw new Blib_Exception_Dao($sql, $bind, $e);
        } elseif (!empty($msg)) {
            $e    = new Exception($msg, 100);
            throw new Blib_Exception_Dao($sql, $bind, $e);
        }

        return false;

    }// end _error


    /**
     * cache select only queries
     * @param str $sql
     * @param array $bind
     * @param mixed $result
     * @return bool
     */
    private function _cache($sql, $bind = array(), $result = null)
    {
        // verify input
        if (strpos(strtolower($sql), 'select') === false) {
            throw new Exception('[' . $sql . '] is not a `select` query, it can not be cached.');
        }

        Blib::register($this->_makeKey($sql, $bind), $result);

    }// end _cache


    /**
     * load from cached queries...
     * @param str $sql
     * @param array $bind
     * @return mixed
     */
    private function _loadCache($sql, $bind = array())
    {
        return Blib::get($this->_makeKey($sql, $bind));

    }// end _loadCache


    /**
     * make key for caching
     * @param str $sql
     * @param array $bind
     * @return str
     */
    private function _makeKey($sql, $bind = array())
    {
        return '_SQL_' . md5(serialize(array($sql, $bind)));

    }// end _makeKey


    /**
     * break down the data
     * @param $data
     * @param $tableName
     * @param $type
     * @return array([sql], [bind])
     */
    private function _breakDownData($data, $tableName, $type)
    {
        // break down...
        if (!is_array($data) ||
            empty($data) ||
            empty($tableName)) {
            $this->_error("$type into $tableName", null, 'Data is invalid');
            return false;
        }

        // otherwise, do it...
        $fields = array();
        $bind   = array();
        $holder = array();

        foreach ($data as $field => $val) {
            if ($type == 'insert') {
                $fields[] = $this->nameQuote($field);
            } else {
                $fields[] = $this->nameQuote($field) . ' = ?';
            }
            $bind[]   = $val;
            $holder[] = '?';
        }

        $tableName = $this->nameQuote($tableName);

        // implement...
        switch ($type) {
            case 'insert':
                $sql = 'INSERT INTO ' . $tableName . '(' . implode(', ', $fields) .
                       ') VALUES (' . implode(', ', $holder) . ')';
                break;

            case 'update':
                $sql = 'UPDATE ' . $tableName . ' SET ' . implode(', ', $fields);
                break;

            case 'replace_into':
                $sql = 'REPLACE INTO ' . $tableName . ' SET ' . implode(', ', $fields);
                break;
        }

        return array('sql' => $sql, 'bind' => $bind);

    }// end _breakDownData

}// end Blib_Data_Adaptor_Pdo
?>